package org.btik.lightdev.dev.tcp;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.btik.context.thread.SystemControlAble;
import org.btik.light.meta.DefaultDevMo;
import org.btik.light.meta.Dev;
import org.btik.light.meta.DevMo;
import org.btik.light.meta.DevState;
import org.btik.light.meta.base.CmdCode;
import org.btik.light.meta.exception.LightException;
import org.btik.light.server.platform.common.api.service.DevAccessSever;
import org.btik.light.server.platform.manage.DevEventDispatcher;
import org.btik.light.tool.BioUtil;
import org.btik.light.tool.ByteUtil;
import org.btik.lightdev.dev.tcp.auth.DevAuth;


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static org.btik.light.meta.LightDevAttrConstant.ACCESS_CHANNEL_TYPE;

public class BioDevAccessServer extends Thread implements DevAccessSever, SystemControlAble {
    private final Log log = LogFactory.getLog(BioDevAccessServer.class);
    private static final int HELLO_LEN = CmdCode.HELLO.length;

    private volatile boolean runFlag = true;

    private ServerSocket serverSocket;


    private static final int DEV_COUNT_LIMIT = 128;

    private int accessServerPort = 8081;

    private DevEventDispatcher devEventDispatcher;

    private DevAuth devAuth;

    private boolean enable = true;

    public void setAccessServerPort(int accessServerPort) {
        this.accessServerPort = accessServerPort;
    }

    public void setDevEventDispatcher(DevEventDispatcher devEventDispatcher) {
        this.devEventDispatcher = devEventDispatcher;
    }

    public void setDevAuth(DevAuth devAuth) {
        this.devAuth = devAuth;
    }

    private static final ConcurrentHashMap<String, Dev> SN_2_DEV = new ConcurrentHashMap<>();

    private static final ConcurrentHashMap<Integer, String> IP_2_SN = new ConcurrentHashMap<>();

    @Override
    public String getSNFromAddress(SocketAddress devAddress) {
        InetSocketAddress isa = (InetSocketAddress) devAddress;
        return IP_2_SN.get(ByteUtil.ipToInt(isa.getAddress().getAddress()));
    }

    @Override
    public Dev getDev(String sn) {
        if (sn == null) {
            return null;
        }
        return SN_2_DEV.get(sn);
    }

    private final byte[] lock = new byte[0];


    @Override
    public void run() {
        if(!enable) {
            log.info("tcp access server is disabled ");
            shutdown("not enable");
            return;
        }
        log.info("devAccess server started");
        try {
            serverSocket = new ServerSocket(accessServerPort);
            while (runFlag) {
                synchronized (lock) {
                    if (SN_2_DEV.size() >= DEV_COUNT_LIMIT) {
                        lock.wait();
                        if (!runFlag) {
                            break;
                        }
                    }
                    Socket client = null;
                    try {
                        client = serverSocket.accept();
                        log.info("new client" + client);
                        if (!devAuth.isAuth(client)) {
                            closeClient("auth failed", client);
                            continue;
                        }
                        DevMo devMo = hello(client);
                        String sn = devMo.getSn();
                        SocketAddress devAddress = client.getRemoteSocketAddress();
                        InetSocketAddress isa = (InetSocketAddress) devAddress;
                        IP_2_SN.put(ByteUtil.ipToInt(isa.getAddress().getAddress()), sn);
                        SN_2_DEV.put(sn, new BioDev(client));
                        devEventDispatcher.onConnect(devMo);
                    } catch (SocketException e) {
                        if (serverSocket.isClosed()) {
                            break;
                        }
                        log.error("", e);
                    } catch (SocketTimeoutException | LightException e) {
                        log.error("e", e);
                        if (client != null) {
                            closeClient(e.getMessage(), client);
                        }
                    }
                }

            }
        } catch (InterruptedException e) {
            log.error("break wait");
        } catch (IOException e) {
            throw new LightException("start DevAccessSever failed", e);
        }
    }

    private void closeClient(String msg, Socket client) {
        try (client) {
            OutputStream outputStream = client.getOutputStream();
            // 发送 debug消息
            outputStream.write(("id:0\nop:0\n" + msg + '\n').getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        } catch (IOException e) {
            log.error("", e);
        }
    }


    /**
     * 握手
     * <pre>
     *     hello\r\n
     *     [snLen(a byte)][sn(snLen bytes)]
     * </pre>
     */
    private DevMo hello(Socket socket) throws IOException {
        socket.setSoTimeout(3000);
        // 不能阻塞单线程
        InputStream reader = socket.getInputStream();
        byte[] helloHead = new byte[HELLO_LEN];
        int read = reader.read(helloHead);
        if (read < 0) {
            throw new LightException("Illegal handshake parameters :hello len");
        }
        if (!Arrays.equals(helloHead, CmdCode.HELLO)) {
            throw new LightException("not hello");
        }
        if (!BioUtil.readNewline(reader)) {
            throw new LightException("Illegal handshake parameters :without new line after hello");
        }
        int snLen = reader.read();
        if (snLen < 1) {
            throw new LightException("Illegal handshake parameters :Illegal length of sn: " + snLen);
        }
        String sn = ByteUtil.toHexString(BioUtil.read(reader, snLen));
        if (!BioUtil.readNewline(reader)) {
            throw new LightException("Illegal handshake parameters :without new line after sn");
        }
        Map<String, String> attrs = readAttr(reader);
        socket.setSoTimeout(0);

        DevMo devMo = new DefaultDevMo(sn, DevState.CONNECTED);
        attrs.put(ACCESS_CHANNEL_TYPE, AccessChannelType.TCP.name());
        devMo.setExtendedAttributes(attrs);
        return devMo;
    }

    private Map<String, String> readAttr(InputStream reader) throws IOException {
        int attrCount = BioUtil.readUnsignedShort(reader);
        // devType和version 是必要的
        if (attrCount < 2) {
            throw new LightException("Illegal message");
        }
        if (!BioUtil.readNewline(reader)) {
            throw new LightException("Illegal handshake parameters :without new line after attrCount");
        }

        ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
        Map<String, String> props = new HashMap<>();
        for (int i = 0; i < attrCount; i++) {
            byte[] key = BioUtil.readUntil(byteBuffer, reader, CmdCode.COLON);
            if (Objects.isNull(key)) {
                throw new LightException("attr count miss");
            }
            byte[] value = BioUtil.readUntil(byteBuffer, reader, CmdCode.NEW_LINE);
            if (null == value) {
                continue;
            }
            // 删除\r
            int length = value.length;
            if (length > 1 && CmdCode.CARRIAGE_RETURN == value[length - 1]) {
                length--;
            }
            props.put(new String(key), new String(value, 0, length));
        }

        if (!BioUtil.readNewline(reader)) {
            throw new LightException("Illegal handshake parameters :without new line after attrs");
        }

        return props;
    }


    public void shutdown(String msg) {
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                log.error("", e);
            }
        }
        synchronized (lock) {
            runFlag = false;
            lock.notify();
        }

    }


    public BioDevAccessServer() {
        super("access-server");
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }
}
